﻿using iTool.ClusterComponent;
using iTool.ClusterComponent.Model;
using iTool.Common;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Orleans.Runtime;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;


namespace iTool.Dashboard.EmbeddedAssets
{
    public sealed class DashboardMiddleware
    {
        private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };

        static DashboardMiddleware()
        {
            Options.Converters.Add(new TimeSpanConverter());
        }

        private const int REMINDER_PAGE_SIZE = 50;
        private const int UNAVAILABLE_RETRY_DELAY = 1;
        private readonly RequestDelegate next;
        
        private IDashboardClient client;

        public DashboardMiddleware(RequestDelegate next)
        {
            this.next = next;
        }

        public async Task Invoke(HttpContext context)
        {

            var request = context.Request;

            var client = this.client;

            if (client is null)
            {
                IClusterService clusterService = iBox.GetService<IClusterService>("IDashboardService");
                if (clusterService == null)
                {
                    context.Response.StatusCode = 200;
                    await context.Response.WriteAsync("no");
                    return;
                }
                this.client = client = new DashboardClient(clusterService);
            }

            IAssetProvider assetProvider = iBox.GetService<IAssetProvider>("IDashboardService");

            try
            {

                if (request.Path == "/" || string.IsNullOrEmpty(request.Path))
                {
                    await WriteIndexFile(context);
                    return;
                }
                if (request.Path == "/favicon.ico")
                {
                    await WriteFileAsync(context, "favicon.ico", "image/x-icon");

                    return;
                }
                if (request.Path == "/index.min.js")
                {
                    await WriteFileAsync(context, "index.min.js", "application/javascript");

                    return;
                }

                if (request.Path == "/version")
                {
                    await WriteJson(context, new { version = typeof(DashboardMiddleware).Assembly.GetName().Version.ToString() });

                    return;
                }

                if (request.Path.StartsWithSegments("/webfonts", out var name))
                {
                    await assetProvider.ServeAssetAsync(name.Value[1..], context);

                    return;
                }

                if (request.Path.StartsWithSegments("/assets", out var fontName))
                {
                    await assetProvider.ServeAssetAsync(fontName.Value[1..], context);

                    return;
                }

                if (request.Path == "/DashboardCounters")
                {
                    var result = await client.DashboardCounters();

                    await WriteJson(context, result.Value);

                    return;
                }

                if (request.Path == "/ClusterStats")
                {
                    var result = await client.ClusterStats();

                    await WriteJson(context, result.Value);

                    return;
                }

                if (request.Path == "/Reminders")
                {
                    try
                    {
                        var result = await client.GetReminders(1, REMINDER_PAGE_SIZE);

                        await WriteJson(context, result.Value);
                    }
                    catch
                    {
                        // if reminders are not configured, the call to the grain will fail
                        await WriteJson(context, new ReminderResponse { Reminders = Array.Empty<ReminderInfo>(), Count = 0 });
                    }

                    return;
                }

                if (request.Path.StartsWithSegments("/Reminders", out var pageString1) && int.TryParse(pageString1.ToValue(), out var page))
                {
                    try
                    {
                        var result = await client.GetReminders(page, REMINDER_PAGE_SIZE);

                        await WriteJson(context, result.Value);
                    }
                    catch
                    {
                        // if reminders are not configured, the call to the grain will fail
                        await WriteJson(context, new ReminderResponse { Reminders = Array.Empty<ReminderInfo>(), Count = 0 });
                    }

                    return;
                }

                if (request.Path.StartsWithSegments("/HistoricalStats", out var remaining))
                {
                    var result = await client.HistoricalStats(remaining.ToValue());

                    await WriteJson(context, result.Value);

                    return;
                }

                if (request.Path.StartsWithSegments("/SiloProperties", out var address1))
                {
                    var result = await client.SiloProperties(address1.ToValue());

                    await WriteJson(context, result.Value);

                    return;
                }

                if (request.Path.StartsWithSegments("/SiloStats", out var address2))
                {
                    var result = await client.SiloStats(address2.ToValue());

                    await WriteJson(context, result.Value);

                    return;
                }

                if (request.Path.StartsWithSegments("/SiloCounters", out var address3))
                {
                    var result = await client.GetCounters(address3.ToValue());

                    await WriteJson(context, result.Value);

                    return;
                }

                if (request.Path.StartsWithSegments("/GrainStats", out var grainName1))
                {
                    var result = await client.GrainStats(grainName1.ToValue());

                    await WriteJson(context, result.Value);

                    return;
                }

                if (request.Path == "/TopGrainMethods")
                {
                    var result = await client.TopGrainMethods();
                    await WriteJson(context, result.Value);
                    return;
                }

                if (request.Path == "/CloudFunctions")
                {
                    var result = await client.GetCloudFunctionTracing();
                    await WriteJson(context, result.Value);
                    return;
                }

                if (request.Path == "/ConnectionStatistics")
                {
                    var result = await client.GetConnectionStatistics();
                    await WriteJson(context, result);
                    return;
                }

                if (request.Path == "/HttpRequestStatistics")
                {
                    var result = await client.GetHttpRequestStatistics();
                    await WriteJson(context, result.Value);
                    return;
                }

                if (request.Path == "/GatewayStatistic")
                {
                    var result = await client.GetGatewayStatisticOptions();

                    await WriteJson(context, result);
                    return;
                }
            }
            catch (SiloUnavailableException)
            {
                this.client = null;
                await WriteUnavailable(context, true);
                return;
            }

            await next(context);
        }

        private static async Task WriteUnavailable(HttpContext context, bool lostConnectivity)
        {
            context.Response.StatusCode = 503; // Service Unavailable
            context.Response.ContentType = "text/plain";
            context.Response.Headers["Retry-After"] = UNAVAILABLE_RETRY_DELAY.ToString();

            if (lostConnectivity)
            {
                await context.Response.WriteAsync("The dashboard has lost connectivity with the Orleans cluster");
            }
            else
            {
                await context.Response.WriteAsync("The dashboard is still trying to connect to the Orleans cluster");
            }
        }

        private static async Task WriteJson<T>(HttpContext context, T content)
        {
            context.Response.StatusCode = 200;
            context.Response.ContentType = "text/json";
            await context.Response.WriteAsync(JsonSerializer.Serialize(content, Options));
        }

        private static async Task WriteFileAsync(HttpContext context, string name, string contentType)
        {
            var assembly = typeof(DashboardMiddleware).GetTypeInfo().Assembly;

            context.Response.StatusCode = 200;
            context.Response.ContentType = contentType;

            var stream = OpenFile(name, assembly);

            using (stream)
            {
                await stream.CopyToAsync(context.Response.Body);
            }
        }

        private async Task WriteIndexFile(HttpContext context)
        {
            var assembly = typeof(DashboardMiddleware).GetTypeInfo().Assembly;

            context.Response.StatusCode = 200;
            context.Response.ContentType = "text/html";

            var stream = OpenFile("Index.html", assembly);

            using (stream)
            {
                var content = new StreamReader(stream).ReadToEnd();

                var basePath = context.Request.PathBase.ToString();

                if (basePath != "/")
                {
                    basePath += "/";
                }

                content = content.Replace("{{BASE}}", basePath);
                content = content.Replace("{{HIDE_TRACE}}", string.Empty);
                content = content.Replace("{{CUSTOM_CSS}}", string.Empty);

                await context.Response.WriteAsync(content);
            }
        }

        private static Stream OpenFile(string name, Assembly assembly)
        {
            var file = new FileInfo(name);

            return file.Exists
                ? file.OpenRead()
                : assembly.GetManifestResourceStream($"iTool.Dashboard.EmbeddedAssets.wwwroot.{name}");
        }
    }
}